// 与进程调度相关的代码
#include "types.h"
#include "common.h"
#include "spinlock.h"
#include "proc.h"
#include "riscv.h"
#include "memlayout.h"

extern char trampoline[];

struct proc procs[NPROC];      // 进程表
struct cpu cpus[NCPU];         // CPU表
int nextpid = 1;               // 下一个可用的进程id
struct spinlock pidlock;       // pid分配锁

// 映射每一个进程的内核栈（内核页表中）
void procstacksmap(pagetable_t kpgtbl) {
    struct proc *p;
    for(p = procs; p < &procs[NPROC]; p++) {
        // 为进程内核栈分配内存空间
        char *stack_pa = kalloc();
        if(stack_pa == 0) {
            panic("进程内核栈分配失败！");
        }
        // 内核栈虚拟地址
        uint64 stack_va = KSTACK((int)(p - procs));
        // 映射，权限为读写
        kvmmap(kpgtbl, stack_va, (uint64)stack_pa, PGSIZE, PTE_R | PTE_W);
    }
}
// 获取当前CPU的信息
struct cpu* getcpu(void) {
    int hartid = r_tp();
    return &cpus[hartid];
}

// 获取当前进程的信息
struct proc* getproc(void) {
    intr_push();
    struct cpu *c = getcpu();
    struct proc *p = c->proc; 
    intr_pop();
    return p;
}
// 初始化进程表
void procinit(void) {
    struct proc* p;
    // 初始化pid分配锁
    initlock(&pidlock, "pidlock");
    // 遍历进程表
    for(p = procs; p < &procs[NPROC]; p++) {
        // 初始化进程锁（注意：虽然锁名称都一样，但其指向不同的地址，并不是同一把锁）
        initlock(&p->lock, "proc");
        // 初始化进程状态为未使用
        p->state = UNUSED;
        // 初始化进程内核栈的虚拟地址
        p->kstack = KSTACK((int)(p - procs));
    }
    printf("进程表初始化完成!\n");
}

// 分配一个进程id
int allocpid(void) {
    int pid;
    acquire(&pidlock);
    pid = nextpid;
    nextpid++;
    release(&pidlock);
    return pid;
}

// 释放进程页表
// 先解除trampoline和trapframe的映射，再释放页表，避免破坏共享内存
void freepagetable(pagetable_t pagetable, uint64 sz) {
    // 解除trampoline映射，但不释放物理页
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    // 解除trapframe映射，但不释放物理页（因为在freeproc中释放）
    uvmunmap(pagetable, TRAPFRAME, 1, 0);
    // 释放进程页表，包括叶子页和中间页
    uvmfree(pagetable, sz);
}

// 释放一个进程
// 包括进程控制块以及相关资源
void freeproc(struct proc *p) {
    // 释放trapframe
    if(p->trapframe) {
        kfree(p->trapframe);    
    }
    p->trapframe = 0;
    // 释放进程页表
    if(p->pagetable) {
        freepagetable(p->pagetable, p->sz);
    }
    p->pagetable = 0;
    // 清空其他参数
    p->sz = 0;
    p->pid = 0;
    p->parent = 0;
    p->name[0] = 0;
    //p->killed = 0;
    //p->xstate = 0;
    p->state = UNUSED;
    //p->chan = 0;
}

// 创建进程页表，并做初始化映射
pagetable_t makepagetable(struct proc *p) {
    // 先创建一个空进程页表
    pagetable_t pagetable;
    pagetable = uvmmake();
    if(pagetable == 0) {
        return 0;
    }
    // 映射trampolinie页，可读可执行，用于用户态与内核态的切换
    // 所有进程虚拟地址和物理地址相同
    if((mappage(pagetable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X)) != 0) {
        // 如果映射失败，则释放页表
        // sz参数为0，表示不释放叶子页，因为此时页表实际上是空的
        uvmfree(pagetable, 0);
        return 0;
    }
    // 映射trapframe页面到trampoline的下方，可读可写，用于保存用户态的上下文
    if((mappage(pagetable, TRAPFRAME, (uint64)p->trapframe, PGSIZE, PTE_R | PTE_W)) != 0) {
        // 如果映射失败，则释放页表
        // sz参数为0，表示不释放叶子页，因为此时页表中只映射了trampoline
        uvmfree(pagetable, 0);
        return 0;
    }
    return pagetable;
}

// 新进程被创建时，其上下文的返回地址被设置为forkret
// 调度器调度到该进程时，将执行forkret函数
// 其中释放调度器加的进程锁并返回用户态
void forkret(void) {
    // 释放从调度器传来的锁
    release(&getproc()->lock);

    // 返回用户态
    usertrapret();
}
// 分配一个进程控制块
struct proc* allocproc(void) {
    struct proc* p;
    // 遍历进程表
    for(p = procs; p < &procs[NPROC]; p++) {
        // 加进程锁分配
        acquire(&p->lock);
        // 如果进程状态为未使用，则分配一个进程控制块
        if(p->state == UNUSED) {
            // 分配进程id
            p->pid = allocpid();
            // 设置进程状态为使用中
            p->state = USED;
            // 分配trapframe页面(物理页)
            if((p->trapframe = (struct trapframe*)kalloc()) == 0) {
                // 如果分配失败，则释放进程
                freeproc(p);
                release(&p->lock);
                return 0;
            }
            // 创建初始进程页表
            p->pagetable = makepagetable(p);
            if(p->pagetable == 0) {
                // 如果创建失败，则释放进程
                freeproc(p);
                release(&p->lock);
                return 0;
            }
            // 设置进程上下文
            memset(&p->context, 0, sizeof(p->context));
            p->context.ra = (uint64)forkret;
            p->context.sp = p->kstack + PGSIZE;

            return p;
        } else {
            // 释放进程锁
            release(&p->lock);
        }
    }
    return 0;
}

// 第一个用户进程的初始化代码指令
uint8 initcode[] = {
    0x13, 0x05, 0xb0, 0x07, 0x93, 0x05, 0x80, 0x1c, 0x33, 0x06, 0xb5, 0x00,
    0x93, 0x08, 0x10, 0x00, 0x73, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00
};
// 初始化第一个用户进程
void procinituser(void) {
    // 分配一个进程控制块
    struct proc  *p;
    p = allocproc();
    // 为首个用户进程分配页面并将指令复制到页面中
    uvmfirst(p->pagetable, initcode, sizeof(initcode));
    p->sz = PGSIZE;
    // 设置进程返回后的指令地址为0（进程空间的虚拟地址，从0x0位置开始执行指令）
    p->trapframe->epc = 0;
    // 设置进程的普通栈顶指针为PGSIZE，即给其分配一页，栈从页顶开始
    p->trapframe->sp = PGSIZE;
    // 设置进程状态为就绪状态，即可被调度
    p->state = RUNNABLE;
    // 释放进程锁
    release(&p->lock);

    printf("第一个用户进程初始化完成!\n");
}

// 进程调度器
// 每个CPU完成初始化后都启动进程调度器，进程调度器永远不会结束
// 进程调度器的作用是从进程表中选择一个就绪进程，调用swtch函数保存旧进程的上下文，并切换到新进程的上下文
// swtch调用ret指令跳转到新进程的ra程序返回地址处，即完成进程切换
void scheduler(void) {
    printf("hart%d:启动调度器！\n", r_tp());
    struct proc *p;
    struct cpu *c = getcpu();
    // 清空cpu上正在运行的进程
    c->proc = 0;
    // 不断循环遍历进程表，搜索就绪进程
    for(;;) {
        // 最近运行的进程可能关闭中断，开启中断避免所有进程等待导致死锁
        intr_on();
        // 记录是否有进程被调度
        int count = 0;
        // 遍历进程表
        for(p = procs; p < &procs[NPROC]; p++) {
            // 加进程锁
            acquire(&p->lock);
            if(p->state == RUNNABLE) {
                // 如果有进程就绪，则调度它
                c->proc = p;
                p->state = RUNNING;
                // 切换进程上下文
                // 切换到新进程后，由新进程负责释放进程锁，执行完返回调度器之前再上锁
                swtch(&c->context, &p->context);
                // 进程执行完毕，返回调度器
                c->proc = 0;
                count = 1;
            }
            // 释放进程锁
            release(&p->lock);
        }
        if(count == 0) {
            // 如果没有就绪进程则进入低功耗等待状态
            intr_on();
            wfi();
        }
    }
}

// 判断进程是否被杀死
int killed(struct proc *p) {
    int iskilled;
    // 加锁访问，避免读取到正在修改的数据
    acquire(&p->lock);
    iskilled = p->killed;
    release(&p->lock);
    return iskilled;
}
// 切换到进程调度器
void sched(void) {
    struct proc *p = getproc();
    // 切换到调度器进程前进行一系列必要的检查
    // 恢复调度器上下文，即CPU中保存的上下文
    // 与调度器中的交换顺序正好相反
    swtch(&p->context, &getcpu()->context);
    
}
// 进程退出（退出状态）
void exit(int status) {
    struct proc *p = getproc();
    // 因为返回调度器之后需要释放锁，所以这里先加锁
    // 由调度器中的逻辑决定，调度器先加锁，再判断进程是否就绪，
    // 如果就绪就会调度进程，返回用户空间的时候释放了进程锁
    // 这里从用户空间退出进程，需要配合调度器释放锁的逻辑
    acquire(&p->lock);
    // 设置进程状态为僵尸状态
    p->state = ZOMBIE;
    // 跳转到进程调度器
    sched();
    panic("exit：返回调度器失败");
}